#!/usr/bin/env python3
"""
音频格式批量转换工具 GUI 版本（支持视频转音频）
3.0版本增加输入音频格式筛选可以指定转换某个类型的文件。
4.0版本 增加了倍速播放。
4.1 进一步优化了ui设计  源文件默认盒程序在同一路径 可以使用软件打包一件运行
作者：鑫盛的苏 sly1@vip.qq.com
"""

# --------------------------------------------------
# 标准库
# --------------------------------------------------
import os
import sys
import platform
import shutil
import subprocess
from datetime import datetime
from pathlib import Path
from typing import (
    Dict, List, Optional, Set, Tuple
)

# --------------------------------------------------
# 第三方音频处理库
# --------------------------------------------------
from mutagen import File as MutagenFile
from mutagen.flac import FLAC
from mutagen.id3 import ID3, APIC
from mutagen.mp4 import MP4, MP4Cover
from mutagen.oggvorbis import OggVorbis

# --------------------------------------------------
# PyQt5 —— 核心
# --------------------------------------------------
from PyQt5.QtCore import (
    QPropertyAnimation, QEasingCurve, Qt, QThread, QUrl, pyqtSignal
)
from PyQt5.QtGui import QDesktopServices, QFont, QIcon, QPalette, QColor

# --------------------------------------------------
# PyQt5 —— 控件
# --------------------------------------------------
from PyQt5.QtWidgets import (
    QApplication, QCheckBox, QComboBox, QDialog, QDialogButtonBox,
    QFileDialog, QFrame, QGridLayout, QGroupBox, QHBoxLayout, QLabel,
    QLineEdit, QMainWindow, QMessageBox, QProgressBar, QPushButton,
    QRadioButton, QTextEdit, QVBoxLayout, QWidget, QSizePolicy, QSplitter
)

# --------------------------------------------------
# 常量定义
# --------------------------------------------------
# 音频格式
AUDIO_EXTS = {".mp3", ".flac", ".wav", ".ogg", ".opus", ".m4a", ".aac", ".wma"}

# 视频格式（支持转换为音频）
VIDEO_EXTS = {
    ".mp4", ".mkv", ".avi", ".mov", ".flv", ".wmv", ".webm",
    ".3gp", ".m4v", ".mpg", ".mpeg", ".vob", ".ts"
}

# 所有支持的输入格式
ALL_INPUT_EXTS = AUDIO_EXTS | VIDEO_EXTS

BITRATE_OPTIONS = ["128k", "192k", "256k", "320k", "500k", "1m"]
SAMPLE_RATE_OPTIONS = ["保持原样", "22050", "44100", "48000", "96000"]
CHANNELS_OPTIONS = ["保持原样", "1 (单声道)", "2 (立体声)"]
THREADS_OPTIONS = ["自动", "1", "2", "4", "8"]
SAME_FORMAT_ACTIONS = ["复制", "跳过", "移动", "转码"]  # 添加转码选项
FORMAT_OPTIONS = ["mp3", "flac", "wav", "ogg", "opus", "m4a", "aac"]


# 文件类型选项映射
FILE_TYPE_OPTIONS = {
    "全部媒体文件": ALL_INPUT_EXTS,
    "音频文件": AUDIO_EXTS,
    "视频文件": VIDEO_EXTS,
    "MP3 (.mp3)": {".mp3"},
    "FLAC (.flac)": {".flac"},
    "WAV (.wav)": {".wav"},
    "OGG Vorbis (.ogg)": {".ogg"},
    "Opus (.opus)": {".opus"},
    "AAC/M4A (.m4a, .aac)": {".m4a", ".aac"},
    "WMA (.wma)": {".wma"},
    "MP4 (.mp4)": {".mp4"},
    "MKV (.mkv)": {".mkv"},
    "AVI (.avi)": {".avi"},
    "MOV (.mov)": {".mov"},
    "FLV (.flv)": {".flv"},
    "WMV (.wmv)": {".wmv"},
    "WebM (.webm)": {".webm"},
    "自定义选择": set()
}


class FileTypeDialog(QDialog):
    def __init__(self, parent=None, selected_exts=None):
        super().__init__(parent)
        self.setWindowTitle("选择要转换的文件类型")
        self.setMinimumWidth(600)
        self.selected_exts = selected_exts or set()
        self.updating = False  # 添加更新标志防止递归更新

        # 主布局
        self.layout = QVBoxLayout(self)
        self.layout.setSpacing(15)
        self.layout.setContentsMargins(20, 20, 20, 15)

        # 顶部说明
        self.layout.addWidget(QLabel("请选择要处理的文件类型："))

        # -------------- 分组 --------------
        # 1. 媒体大类 - 水平排列
        group_layout = QHBoxLayout()
        group_layout.setSpacing(30)

        # 添加三个大类
        self.grp_all = self._add_group("全部媒体文件", ALL_INPUT_EXTS)
        self.grp_audio = self._add_group("音乐文件", AUDIO_EXTS)
        self.grp_video = self._add_group("视频文件", VIDEO_EXTS)

        group_layout.addWidget(self.grp_all.check)
        group_layout.addWidget(self.grp_audio.check)
        group_layout.addWidget(self.grp_video.check)

        self.layout.addLayout(group_layout)

        # -------------- 明细区域 --------------
        detail_layout = QHBoxLayout()
        detail_layout.setSpacing(30)

        # 左侧：音频格式明细
        audio_frame = QFrame()
        audio_frame.setFrameShape(QFrame.StyledPanel)
        audio_layout = QVBoxLayout(audio_frame)
        audio_layout.setContentsMargins(15, 15, 15, 15)

        audio_layout.addWidget(QLabel("<b>音乐格式明细</b>"))
        self.audio_checks = {
            ext: self._add_check(f"{ext.upper()[1:]} ({ext})", {ext})
            for ext in sorted(AUDIO_EXTS)
        }
        for chk in self.audio_checks.values():
            audio_layout.addWidget(chk)
        # 让多余空间留在下方
        audio_layout.addStretch()

        # 右侧：视频格式明细
        video_frame = QFrame()
        video_frame.setFrameShape(QFrame.StyledPanel)
        video_layout = QVBoxLayout(video_frame)
        video_layout.setContentsMargins(15, 15, 15, 15)

        video_layout.addWidget(QLabel("<b>视频格式明细</b>"))
        self.video_checks = {
            ext: self._add_check(f"{ext.upper()[1:]} ({ext})", {ext})
            for ext in sorted(VIDEO_EXTS)
        }
        for chk in self.video_checks.values():
            video_layout.addWidget(chk)
        video_layout.addStretch()

        # 关键：两列顶部对齐
        detail_layout.addWidget(audio_frame, alignment=Qt.AlignTop)
        detail_layout.addWidget(video_frame, alignment=Qt.AlignTop)

        self.layout.addLayout(detail_layout)

        # -------------- 支持的格式文本 --------------
        support_text = QLabel(f"支持格式: {', '.join(sorted(ALL_INPUT_EXTS))}")
        support_text.setStyleSheet("color: #666; font-size: 10px;")
        self.layout.addWidget(support_text)

        # -------------- 按钮 --------------
        btns = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        btns.accepted.connect(self.accept)
        btns.rejected.connect(self.reject)
        self.layout.addWidget(btns)

        # -------------- 信号连接 --------------
        # 大类联动明细
        self.grp_all.check.stateChanged.connect(self._on_all_changed)
        self.grp_audio.check.stateChanged.connect(self._on_audio_group_changed)
        self.grp_video.check.stateChanged.connect(self._on_video_group_changed)

        # 明细联动大类
        for chk in self.audio_checks.values():
            chk.stateChanged.connect(self._sync_groups_from_details)
        for chk in self.video_checks.values():
            chk.stateChanged.connect(self._sync_groups_from_details)

        # 初始化选中状态
        self._restore_selection()

    # --------------------------------------------------
    # 内部工具
    # --------------------------------------------------
    def _add_group(self, title: str, exts: set) -> object:
        """返回一个包装了 QCheckBox 的对象，方便统一处理。"""
        check = QCheckBox(title)
        check.exts = exts
        # 创建动画（透明度）
        anim = QPropertyAnimation(check, b"windowOpacity")
        anim.setDuration(180)
        setattr(check, "_anim", anim)  # 挂到对象上
        return type("Group", (), {"check": check, "exts": exts})()

    def _add_check(self, text: str, exts: set):
        chk = QCheckBox(text)
        chk.exts = exts
        return chk

    # --------------------------------------------------
    # 优化后的联动逻辑
    # --------------------------------------------------
    def _on_all_changed(self, state):
        """'全部媒体文件'被切换"""
        if self.updating:
            return

        self.updating = True
        if state == Qt.Checked:
            # 选中所有小类
            for chk in (*self.audio_checks.values(), *self.video_checks.values()):
                chk.setChecked(True)
            # 更新大类状态
            self.grp_audio.check.setChecked(True)
            self.grp_video.check.setChecked(True)
        else:
            # 全部取消 - 注意：这里不自动取消小类
            # 保持当前的小类选择状态
            self.grp_audio.check.setChecked(False)
            self.grp_video.check.setChecked(False)
        self.updating = False

    def _on_audio_group_changed(self, state):
        """'音频文件'被切换"""
        if self.updating:
            return

        self.updating = True
        for chk in self.audio_checks.values():
            chk.setChecked(state == Qt.Checked)
        self._sync_all_from_sub()
        self.updating = False

    def _on_video_group_changed(self, state):
        """'视频文件'被切换"""
        if self.updating:
            return

        self.updating = True
        for chk in self.video_checks.values():
            chk.setChecked(state == Qt.Checked)
        self._sync_all_from_sub()
        self.updating = False

    def _sync_groups_from_details(self):
        """明细改变时，同步大类"""
        if self.updating:
            return

        self.updating = True

        # 计算音频大类状态
        audio_checked = any(chk.isChecked() for chk in self.audio_checks.values())
        audio_all_checked = all(chk.isChecked() for chk in self.audio_checks.values())

        # 计算视频大类状态
        video_checked = any(chk.isChecked() for chk in self.video_checks.values())
        video_all_checked = all(chk.isChecked() for chk in self.video_checks.values())

        # 更新音频大类状态
        if audio_all_checked:
            self.grp_audio.check.setCheckState(Qt.Checked)
        elif audio_checked:
            self.grp_audio.check.setCheckState(Qt.PartiallyChecked)
        else:
            self.grp_audio.check.setCheckState(Qt.Unchecked)

        # 更新视频大类状态
        if video_all_checked:
            self.grp_video.check.setCheckState(Qt.Checked)
        elif video_checked:
            self.grp_video.check.setCheckState(Qt.PartiallyChecked)
        else:
            self.grp_video.check.setCheckState(Qt.Unchecked)

        # 更新全部大类状态
        self._sync_all_from_sub()
        self.updating = False

    def _sync_all_from_sub(self):
        """根据子项同步'全部媒体'"""
        audio_checked = self.grp_audio.check.checkState() != Qt.Unchecked
        audio_all_checked = self.grp_audio.check.checkState() == Qt.Checked

        video_checked = self.grp_video.check.checkState() != Qt.Unchecked
        video_all_checked = self.grp_video.check.checkState() == Qt.Checked

        # 更新全部大类状态
        if audio_all_checked and video_all_checked:
            self.grp_all.check.setCheckState(Qt.Checked)
        elif audio_checked or video_checked:
            self.grp_all.check.setCheckState(Qt.PartiallyChecked)
        else:
            self.grp_all.check.setCheckState(Qt.Unchecked)

    # --------------------------------------------------
    # 初始化 & 导出
    # --------------------------------------------------
    def _restore_selection(self):
        """根据外部传入的 selected_exts 恢复勾选"""
        self.updating = True

        # 恢复小类选择
        for chk in (*self.audio_checks.values(), *self.video_checks.values()):
            if chk.exts.issubset(self.selected_exts):
                chk.setChecked(True)

        # 更新大类状态
        self.updating = False
        self._sync_groups_from_details()

    def selected_extensions(self) -> set:
        """返回用户最终勾选的扩展名集合"""
        ext = set()
        for chk in (*self.audio_checks.values(), *self.video_checks.values()):
            if chk.isChecked():
                ext |= chk.exts
        return ext


class ConversionThread(QThread):
    progress_updated = pyqtSignal(int, int, str)
    conversion_complete = pyqtSignal(str, str)  # 添加输出目录参数
    error_occurred = pyqtSignal(str)

    def __init__(self, config):
        super().__init__()
        self.config = config
        self.cancel_requested = False
        self.output_dir = None  # 存储输出目录

    def run(self):
        try:
            in_path = Path(self.config["input_path"]).expanduser().resolve()
            if not in_path.exists():
                self.error_occurred.emit(f"路径不存在: {in_path}")
                return

            files = self.list_media_files(in_path)
            if not files:
                self.error_occurred.emit("未找到任何媒体文件")
                return

            out_ext = self.config["output_fmt"].lower().lstrip(".")
            cover_file = Path(self.config["cover_path"]) if self.config["cover_path"] else None

            # 创建输出目录（如果未指定，则创建时间戳目录）
            if self.config["output_dir"]:
                out_dir = Path(self.config["output_dir"]).expanduser().resolve()
            else:
                root = in_path
                if in_path.is_file():
                    root = in_path.parent
                now = datetime.now()
                ts = now.strftime("%Y%m%d_%H%M%S") + f"_{now.microsecond // 1000:03d}"
                out_dir = root / f"转换完成{ts}"

            out_dir.mkdir(parents=True, exist_ok=True)
            self.output_dir = out_dir  # 保存输出目录

            tasks: List[Tuple[Path, str]] = []  # (src, action)
            total_files = len(files)
            reported = 0  # 已报告的文件数（包括跳过的文件）

            for f in files:
                same_fmt = f.suffix.lower() == f".{out_ext}"
                action = self.config["same_format_action"].lower()

                # 视频文件总是需要转换（转音频）
                is_video = f.suffix.lower() in VIDEO_EXTS

                if same_fmt and not is_video:
                    if action == "跳过":
                        self.progress_updated.emit(reported, total_files,
                                                   f"跳过相同格式文件: {f.name}")
                        reported += 1
                        continue
                    elif action == "移动":
                        tasks.append((f, "move"))
                        self.progress_updated.emit(reported, total_files,
                                                   f"准备移动文件: {f.name}")
                    elif action == "复制":
                        tasks.append((f, "copy"))
                        self.progress_updated.emit(reported, total_files,
                                                   f"准备复制文件: {f.name}")
                    elif action == "转码":  # 处理转码选项
                        tasks.append((f, "transcode"))
                        self.progress_updated.emit(reported, total_files,
                                                   f"准备转码文件(同格式): {f.name}")
                else:
                    tasks.append((f, "transcode"))
                    self.progress_updated.emit(reported, total_files,
                                               f"准备转换文件: {f.name}")

            if not tasks:
                self.error_occurred.emit("没有需要处理的文件")
                return

            total_tasks = len(tasks)
            processed = 0

            # 文件名计数器，用于处理重名
            name_counter = {}

            for src, action in tasks:
                if self.cancel_requested:
                    self.progress_updated.emit(reported, total_files, "任务已取消")
                    return

                try:
                    # 生成唯一输出文件名
                    base_name = src.stem
                    out_name = f"{base_name}.{out_ext}"

                    # 检查文件名是否已存在
                    if out_name in name_counter:
                        name_counter[out_name] += 1
                        out_name = f"{base_name}_{name_counter[out_name]}.{out_ext}"
                    else:
                        name_counter[out_name] = 0

                    dst = self.output_dir / out_name

                    # 确保文件名唯一
                    counter = 1
                    while dst.exists():
                        out_name = f"{base_name}_{counter}.{out_ext}"
                        dst = self.output_dir / out_name
                        counter += 1

                    # 处理文件
                    if action == "transcode":
                        self.convert_one(src, dst, out_ext, self.config["bitrate"],
                                         self.config["sample_rate"], self.config["channels"],
                                         self.config["tags"], src.stem)
                        self.progress_updated.emit(processed, total_tasks,
                                                   f"转换完成: {src.name} -> {out_name}")
                    elif action == "move":
                        self.move_file(src, dst)
                        self.progress_updated.emit(processed, total_tasks,
                                                   f"移动完成: {src.name} -> {out_name}")
                    elif action == "copy":
                        self.copy_file(src, dst)
                        self.progress_updated.emit(processed, total_tasks,
                                                   f"复制完成: {src.name} -> {out_name}")

                    # 嵌入封面（如果是新创建的文件）
                    if action != "move":  # 移动的文件已经存在，不需要重新嵌入封面
                        self.embed_cover(dst, cover_file)

                    processed += 1
                    reported += 1  # 更新已报告的文件数
                except Exception as e:
                    self.progress_updated.emit(processed, total_tasks, f"处理失败: {src.name} -> {str(e)}")
                    reported += 1  # 即使失败也计入进度

            # 传递输出目录给主线程
            output_dir_str = str(self.output_dir) if self.output_dir else "未知目录"
            self.conversion_complete.emit(f"处理完成! 共处理 {processed}/{total_tasks} 个文件", output_dir_str)
        except Exception as e:
            self.error_occurred.emit(f"发生错误: {str(e)}")

    def cancel(self):
        self.cancel_requested = True

    def list_media_files(self, path: Path) -> List[Path]:
        selected_exts = self.config["selected_exts"]

        if path.is_file():
            return [path] if path.suffix.lower() in selected_exts else []
        return [p for p in path.rglob("*") if p.suffix.lower() in selected_exts]

    def convert_one(self, in_file: Path, out_file: Path,
                    fmt: str, bitrate: str, sr: int, ch: int,
                    tags: Dict[str, Optional[str]], src_stem: str) -> None:
        meta_list = []
        for k, v in tags.items():
            if v is None and k.lower() == "title":
                v = src_stem
            elif v is None and k.lower() == "year":
                v = str(datetime.now().year)
            if v:
                meta_list.extend(["-metadata", f"{k}={v}"])

        cmd = [
            "ffmpeg", "-hide_banner", "-loglevel", "error",
            "-y", "-i", str(in_file),
            "-map_metadata", "-1",
            *meta_list,
            "-ar", str(sr or 44100),
            "-ac", str(ch or 2),
            "-threads", str(self.config["threads"] or 0),
        ]

        # 如果是视频文件，添加视频流忽略参数
        if in_file.suffix.lower() in VIDEO_EXTS:
            cmd.extend(["-vn"])  # 忽略视频流，只提取音频

        # 追加播放速度
        if self.config["speed"] != 1.0:
            cmd.extend(["-filter:a", f"atempo={self.config['speed']}"])

        # 根据输出格式设置编码参数
        if fmt == "mp3":
            cmd.extend(["-c:a", "libmp3lame", "-b:a", bitrate])
        elif fmt in {"m4a", "aac"}:
            cmd.extend(["-c:a", "aac", "-b:a", bitrate])
        elif fmt == "opus":
            cmd.extend(["-c:a", "libopus", "-b:a", bitrate])
        elif fmt == "flac":
            cmd.extend(["-c:a", "flac"])
        elif fmt == "ogg":
            cmd.extend(["-c:a", "libvorbis", "-q:a", "5"])
        else:
            cmd.extend(["-c:a", "copy"])

        cmd.append(str(out_file))
        subprocess.run(cmd, check=True)

    def copy_file(self, src: Path, dst: Path) -> None:
        shutil.copy2(src, dst)

    def move_file(self, src: Path, dst: Path) -> None:
        shutil.move(src, dst)

    def embed_cover(self, file_path: Path, cover: Optional[Path]) -> None:
        if not (cover and cover.exists()):
            return

        # 只支持在音频文件中嵌入封面
        if file_path.suffix.lower() not in AUDIO_EXTS:
            return

        mime = "image/jpeg" if cover.suffix.lower() in {".jpg", ".jpeg"} else "image/png"
        with open(cover, "rb") as f:
            data = f.read()

        if file_path.suffix.lower() == ".mp3":
            audio = ID3(str(file_path))
            audio.add(APIC(encoding=3, mime=mime, type=3, desc="Cover", data=data))
            audio.save(v2_version=3)
        elif isinstance(audio := MutagenFile(str(file_path)), FLAC):
            audio.add_picture(data)
            audio.save()
        elif isinstance(audio, MP4):
            fmt = MP4Cover.FORMAT_JPEG if b"JFIF" in data else MP4Cover.FORMAT_PNG
            audio["covr"] = [MP4Cover(data, imageformat=fmt)]
            audio.save()
        elif isinstance(audio, OggVorbis):
            audio["metadata_block_picture"] = [data]
            audio.save()


# 开源协议声明&版权声明：
class AboutLabel(QLabel):
    def __init__(self):
        super().__init__('<a href="#" style="color:#1a73e8; text-decoration:none;">关于本软件</a>')
        self.setOpenExternalLinks(False)
        self.setTextInteractionFlags(Qt.TextBrowserInteraction)
        self.setCursor(Qt.PointingHandCursor)
        self.setStyleSheet("padding-left: 10px;")  # 添加左边距

    def mousePressEvent(self, ev):
        msg = QMessageBox()
        msg.setWindowTitle("关于本软件")
        msg.setTextFormat(Qt.RichText)  # 启用富文本
        msg.setText(
            "<b>媒体文件转音频工具 v4.1</b><br><br>"
            "作者：鑫盛的苏 &lt;sly1@vip.qq.com&gt;<br><br>"
            "<b>基于以下开源组件构建：</b><br>"
            "• Python 3 — PSF 许可证<br>"
            "• PyQt5 — GPL-3.0<br>"
            "• FFmpeg — LGPL-2.1/GPL-2.0 (多媒体处理核心)<br>"
            "• Mutagen — MIT 许可证 (元数据处理)<br><br>"
            "各组件版权归属其相应作者，"
            "源代码及许可证可在各自官网获取：<br>"
            "<a href='https://www.python.org'>https://www.python.org</a><br>"
            "<a href='https://www.riverbankcomputing.com/software/pyqt/'>https://www.riverbankcomputing.com/software/pyqt/</a><br>"
            "<a href='https://ffmpeg.org/download.html'>https://ffmpeg.org/download.html</a><br>"
            "<a href='https://mutagen.readthedocs.io'>https://mutagen.readthedocs.io</a>"
        )


class AudioConverterApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle("媒体文件转音频工具")
        self.setGeometry(100, 100, 600, 600)  # 增加宽度以适应新布局
        self.output_dir = None  # 存储输出目录
        self.selected_exts = ALL_INPUT_EXTS  # 默认选择所有支持的格式

        # 设置应用图标
        if hasattr(sys, '_MEIPASS'):
            icon_path = os.path.join(sys._MEIPASS, 'icon.ico')
        else:
            icon_path = 'icon.ico'

        if os.path.exists(icon_path):
            self.setWindowIcon(QIcon(icon_path))

        self.init_ui()
        self.input_path_edit.setText(os.path.dirname(os.path.abspath(__file__)))  #默认路径为当前软件所在位置
        self.conversion_thread = None

    def init_ui(self):

        # 创建主窗口部件和布局
        main_widget = QWidget()
        main_layout = QVBoxLayout(main_widget)
        main_layout.setContentsMargins(10, 10, 10, 10)
        main_layout.setSpacing(10)

        # 创建分割器，左侧设置区域，右侧日志区域
        splitter = QSplitter(Qt.Horizontal)
        main_layout.addWidget(splitter, 1)

        # 左侧设置区域（垂直布局）
        settings_widget = QWidget()
        settings_layout = QVBoxLayout(settings_widget)
        settings_layout.setContentsMargins(5, 5, 5, 5)
        settings_layout.setSpacing(15)  # 增加间距

        # 设置区域样式
        settings_widget.setStyleSheet("""
            QGroupBox {
                font-weight: bold;
                border: 1px solid #d0d0d0;
                border-radius: 5px;
                margin-top: 1.5ex;  /* 增加上边距 */
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                padding: 0 8px;
                top: -0.5ex;  /* 调整标题位置 */
            }
        """)

        # 输入设置
        input_group = QGroupBox("输入设置（必填）")
        input_layout = QGridLayout()
        input_layout.setVerticalSpacing(8)  # 增加垂直间距
        input_layout.setHorizontalSpacing(8)

        # 输入类型选择
        self.input_type_folder = QRadioButton("整个文件夹")
        self.input_type_file = QRadioButton("单个文件")
        self.input_type_folder.setChecked(True)  # 默认选择文件夹

        input_type_layout = QHBoxLayout()
        input_type_layout.addWidget(QLabel("输入类型:"))
        input_type_layout.addWidget(self.input_type_folder)
        input_type_layout.addWidget(self.input_type_file)

        # 输入路径控件
        self.input_path_edit = QLineEdit()
        input_browse_btn = QPushButton("浏览...")
        input_browse_btn.clicked.connect(self.browse_input_path)

        # 文件类型选择按钮
        self.file_type_btn = QPushButton("选择文件类型...")
        self.file_type_btn.clicked.connect(self.select_file_types)
        self.file_type_btn.setStyleSheet("background-color: #E0E0E0;")
        self.update_file_type_button_text()

        input_layout.addLayout(input_type_layout, 0, 0, 1, 3)
        input_layout.addWidget(QLabel("源文件位置:"), 1, 0)
        input_layout.addWidget(self.input_path_edit, 1, 1)
        input_layout.addWidget(input_browse_btn, 1, 2)
        input_layout.addWidget(QLabel("要转换的文件类型:"), 2, 0)
        input_layout.addWidget(self.file_type_btn, 2, 1, 1, 2)  # 跨两列

        hint = QLabel("提示：支持整个文件夹批量转换；和单个文件转换")
        hint.setWordWrap(True)  # 自动换行
        hint.setStyleSheet("color: #666; font-size: 12px;")
        # 放到第 3 行，跨 3 列
        input_layout.addWidget(hint, 3, 0, 1, 3)

        input_group.setLayout(input_layout)

        # 输出设置
        output_group = QGroupBox("输出设置（建议默认）")
        output_layout = QGridLayout()
        output_layout.setVerticalSpacing(8)  # 增加垂直间距
        output_layout.setHorizontalSpacing(8)

        self.output_dir_edit = QLineEdit()
        output_dir_browse_btn = QPushButton("浏览...")
        output_dir_browse_btn.clicked.connect(self.browse_output_dir)

        self.output_fmt_combo = QComboBox()
        self.output_fmt_combo.addItems(FORMAT_OPTIONS)
        self.output_fmt_combo.setCurrentText("mp3")

        self.bitrate_combo = QComboBox()
        self.bitrate_combo.addItems(BITRATE_OPTIONS)
        self.bitrate_combo.setCurrentText("256k")

        # 更新为包含"转码"选项
        self.same_format_action_combo = QComboBox()
        self.same_format_action_combo.addItems(SAME_FORMAT_ACTIONS)
        self.same_format_action_combo.setCurrentText("复制")

        output_layout.addWidget(QLabel("输出目录:"), 0, 0)
        output_layout.addWidget(self.output_dir_edit, 0, 1)
        output_layout.addWidget(output_dir_browse_btn, 0, 2)

        output_layout.addWidget(QLabel("输出格式:"), 1, 0)
        output_layout.addWidget(self.output_fmt_combo, 1, 1)

        output_layout.addWidget(QLabel("比特率:"), 2, 0)
        output_layout.addWidget(self.bitrate_combo, 2, 1)

        output_layout.addWidget(QLabel("同格式处理:"), 3, 0)
        output_layout.addWidget(self.same_format_action_combo, 3, 1)


        # -------------- 基础加速 --------------
        SPEED_OPTIONS = ["0.5", "0.75", "1.0", "1.25", "1.5", "2.0"]
        speed_layout = QHBoxLayout()
        speed_layout.setSpacing(10)

        self.speed_combo = QComboBox()
        self.speed_combo.addItems(SPEED_OPTIONS)
        self.speed_combo.setCurrentText("1.0")
        self.speed_combo.setFixedWidth(70)  # 缩短宽度

        # -------------- 超级加速 --------------
        self.super_speed_combo = QComboBox()
        self.super_speed_combo.addItems(["1",  "2", "3", "4", "5", "6"])
        self.super_speed_combo.setCurrentText("1")
        self.super_speed_combo.setFixedWidth(70)  # 缩短宽度
        # ↓↓↓ 新增 ↓↓↓
        self._last_super_speed = "1"   # 弹窗提示超过2倍速的情况
        self.super_speed_combo.currentTextChanged.connect(self._on_super_speed_changed)


        speed_layout.addWidget(QLabel("音频加速:"))
        speed_layout.addWidget(self.speed_combo)
        speed_layout.addWidget(QLabel("超级加倍:"))
        speed_layout.addWidget(self.super_speed_combo)
        speed_layout.addStretch()  # 右侧弹性空间

        output_layout.addLayout(speed_layout, 4, 0, 1, 3)  # 跨三列
        output_group.setLayout(output_layout)

        # 添加提示标签
        self.video_note_label = QLabel("注意: 视频文件将提取音频转换为指定格式")
        self.video_note_label.setStyleSheet("color: #d9534f; font-size: 12px;")
        output_layout.addWidget(self.video_note_label, 6, 0, 1, 3)


        # 音频设置
        audio_group = QGroupBox("音频设置（建议默认）")
        audio_layout = QGridLayout()
        audio_layout.setVerticalSpacing(8)  # 增加垂直间距
        audio_layout.setHorizontalSpacing(8)

        self.sample_rate_combo = QComboBox()
        self.sample_rate_combo.addItems(SAMPLE_RATE_OPTIONS)
        self.sample_rate_combo.setCurrentIndex(0)

        self.channels_combo = QComboBox()
        self.channels_combo.addItems(CHANNELS_OPTIONS)
        self.channels_combo.setCurrentIndex(0)

        self.threads_combo = QComboBox()
        self.threads_combo.addItems(THREADS_OPTIONS)
        self.threads_combo.setCurrentIndex(0)

        audio_layout.addWidget(QLabel("采样率:"), 0, 0)
        audio_layout.addWidget(self.sample_rate_combo, 0, 1)

        audio_layout.addWidget(QLabel("声道数:"), 1, 0)
        audio_layout.addWidget(self.channels_combo, 1, 1)

        audio_layout.addWidget(QLabel("线程数:"), 2, 0)
        audio_layout.addWidget(self.threads_combo, 2, 1)

        audio_group.setLayout(audio_layout)

        # 元数据设置
        metadata_group = QGroupBox("元数据设置（选填）")
        metadata_layout = QGridLayout()
        metadata_layout.setVerticalSpacing(8)  # 增加垂直间距
        metadata_layout.setHorizontalSpacing(8)

        self.title_edit = QLineEdit()
        self.artist_edit = QLineEdit()
        self.album_edit = QLineEdit()
        self.year_edit = QLineEdit()
        self.encoded_by_edit = QLineEdit("鑫盛的苏-ffmpeg可视化界面")

        metadata_layout.addWidget(QLabel("标题:"), 0, 0)
        metadata_layout.addWidget(self.title_edit, 0, 1)

        metadata_layout.addWidget(QLabel("艺术家:"), 1, 0)
        metadata_layout.addWidget(self.artist_edit, 1, 1)

        metadata_layout.addWidget(QLabel("专辑:"), 2, 0)
        metadata_layout.addWidget(self.album_edit, 2, 1)

        metadata_layout.addWidget(QLabel("年份:"), 3, 0)
        metadata_layout.addWidget(self.year_edit, 3, 1)

        metadata_layout.addWidget(QLabel("编码者:"), 4, 0)
        metadata_layout.addWidget(self.encoded_by_edit, 4, 1)

        metadata_group.setLayout(metadata_layout)

        # 封面设置
        cover_group = QGroupBox("封面设置（选填）")
        cover_layout = QGridLayout()
        cover_layout.setVerticalSpacing(8)
        cover_layout.setHorizontalSpacing(8)

        # 提示文本
        tip = QLabel("注意: 仅支持音频文件嵌入封面")
        tip.setStyleSheet("color: #d9534f; font-size: 12px;")
        tip.setWordWrap(True)

        # 路径控件
        self.cover_path_edit = QLineEdit()
        cover_browse_btn = QPushButton("浏览...")
        cover_browse_btn.clicked.connect(self.browse_cover)

        # 网格定位
        cover_layout.addWidget(QLabel("封面路径:"), 0, 0)
        cover_layout.addWidget(self.cover_path_edit, 0, 1)
        cover_layout.addWidget(cover_browse_btn, 0, 2)
        cover_layout.addWidget(tip, 1, 0, 1, 3)  # 提示占整行

        cover_group.setLayout(cover_layout)


        # 添加到左侧设置区域
        settings_layout.addWidget(input_group)
        settings_layout.addWidget(output_group)
        settings_layout.addWidget(audio_group)
        settings_layout.addWidget(metadata_group)
        settings_layout.addWidget(cover_group)
        settings_layout.addStretch()  # 添加弹性空间


        # 右侧日志区域（垂直布局）
        log_widget = QWidget()
        log_layout = QVBoxLayout(log_widget)
        log_layout.setContentsMargins(5, 5, 5, 5)
        log_layout.setSpacing(15)  # 增加间距

        # 日志区域样式
        log_widget.setStyleSheet("""
            QGroupBox {
                font-weight: bold;
                border: 1px solid #d0d0d0;
                border-radius: 5px;
                margin-top: 1.5ex;  /* 增加上边距 */
            }
            QGroupBox::title {
                subcontrol-origin: margin;
                padding: 0 8px;
                top: -0.5ex;  /* 调整标题位置 */
            }
        """)

        # 日志区域
        log_group = QGroupBox("操作日志")
        log_inner_layout = QVBoxLayout(log_group)
        log_inner_layout.setContentsMargins(10, 15, 10, 10)  # 增加上边距

        self.log_text = QTextEdit()
        self.log_text.setReadOnly(True)
        self.log_text.setFont(QFont("Consolas", 10))
        self.log_text.setStyleSheet("""
            QTextEdit {
                background-color: #f8f8f8;
                border: 1px solid #d0d0d0;
                border-radius: 3px;
                padding: 5px;
                margin-top: 5px;  /* 增加上边距 */
            }
        """)

        log_inner_layout.addWidget(self.log_text)

        # 按钮区域
        button_group = QGroupBox("操作")
        button_layout = QHBoxLayout(button_group)
        button_layout.setContentsMargins(10, 15, 10, 10)  # 增加上边距
        button_layout.setSpacing(15)  # 增加按钮间距

        self.start_btn = QPushButton("开始转换")
        self.start_btn.clicked.connect(self.start_conversion)
        self.start_btn.setMinimumHeight(35)  # 增加按钮高度
        self.start_btn.setStyleSheet("""
            QPushButton {
                background-color: #4CAF50;
                color: white;
                font-weight: bold;
                padding: 8px 16px;
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #45a049;
            }
        """)

        self.cancel_btn = QPushButton("取消")
        self.cancel_btn.clicked.connect(self.cancel_conversion)
        self.cancel_btn.setEnabled(False)
        self.cancel_btn.setMinimumHeight(35)  # 增加按钮高度
        self.cancel_btn.setStyleSheet("""
            QPushButton {
                background-color: #f44336;
                color: white;
                padding: 8px 16px;
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #d32f2f;
            }
        """)

        self.open_dir_btn = QPushButton("打开输出目录")
        self.open_dir_btn.clicked.connect(self.open_output_directory)
        self.open_dir_btn.setEnabled(False)
        self.open_dir_btn.setMinimumHeight(35)  # 增加按钮高度
        self.open_dir_btn.setStyleSheet("""
            QPushButton {
                background-color: #2196F3;
                color: white;
                padding: 8px 16px;
                border-radius: 4px;
            }
            QPushButton:hover {
                background-color: #0b7dda;
            }
        """)

        button_layout.addWidget(self.start_btn)
        button_layout.addWidget(self.cancel_btn)
        button_layout.addWidget(self.open_dir_btn)

        # 添加到右侧日志区域

        log_layout.addWidget(log_group, 1)  # 日志区域占据更多空间
        log_layout.addWidget(button_group)

        # 添加到分割器
        splitter.addWidget(settings_widget)
        splitter.addWidget(log_widget)
        splitter.setSizes([350, 650])  # 调整左右比例
        splitter.setHandleWidth(8)  # 增加分割条宽度


        # ===== 底部进度条 =====
        bottom_progress = QGroupBox("转换进度")
        bottom_layout = QVBoxLayout(bottom_progress)
        bottom_layout.setContentsMargins(10, 10, 10, 10)
        # 进度条
        self.progress_bar = QProgressBar()
        self.progress_bar.setRange(0, 100)
        self.progress_bar.setValue(0)
        self.progress_bar.setFormat("%v/%m 个文件 (%p%)")
        self.progress_bar.setStyleSheet("""
            QProgressBar {
                border: 1px solid #ccc;
                border-radius: 3px;
                text-align: center;
                height: 24px;
            }
            QProgressBar::chunk {
                background-color: #4CAF50;
                width: 10px;
            }
        """)

        bottom_layout.addWidget(self.progress_bar)  # 复用之前的 self.progress_bar
        main_layout.addWidget(bottom_progress)      # 放在最下方
        # ==========================


        self.setCentralWidget(main_widget)

        # 左侧消息
        self.statusBar().showMessage("准备就绪")
        # 右侧“关于”链接
        self.statusBar().addPermanentWidget(AboutLabel())




    def _on_super_speed_changed(self, text):
        base = float(self.speed_combo.currentText())
        super_ = float(text)
        total = base * super_

        # 只在乘积 > 2 且上一次乘积 ≤ 2 时弹一次
        if total > 2 and float(self._last_super_speed) * float(self.speed_combo.currentText()) <= 2:
            QMessageBox.warning(
                self,
                "超级加倍提示",
                "最终倍速 > 2，可能降低音质，请谨慎使用！"
            )
        self._last_super_speed = text


    def select_file_types(self):
        """打开文件类型选择对话框"""
        dialog = FileTypeDialog(self, self.selected_exts)
        if dialog.exec_() == QDialog.Accepted:
            self.selected_exts = dialog.selected_extensions()
            self.update_file_type_button_text()
            self.log_message(f"已选择文件类型: {self.get_selected_exts_text()}")

    def update_file_type_button_text(self):
        """更新文件类型按钮的文本"""
        if len(self.selected_exts) == len(ALL_INPUT_EXTS):
            self.file_type_btn.setText("所有媒体文件")
        elif len(self.selected_exts) == len(AUDIO_EXTS):
            self.file_type_btn.setText("所有音频文件")
        elif len(self.selected_exts) == len(VIDEO_EXTS):
            self.file_type_btn.setText("所有视频文件")
        elif len(self.selected_exts) == 1:
            ext = next(iter(self.selected_exts))
            self.file_type_btn.setText(f"{ext.upper()[1:]} 文件")
        else:
            count = len(self.selected_exts)
            self.file_type_btn.setText(f"{count}种文件类型")

    def get_selected_exts_text(self):
        """获取选中的文件类型的文本表示"""
        if len(self.selected_exts) == len(ALL_INPUT_EXTS):
            return "所有支持的媒体格式"
        elif len(self.selected_exts) == len(AUDIO_EXTS):
            return "所有支持的音频格式"
        elif len(self.selected_exts) == len(VIDEO_EXTS):
            return "所有支持的视频格式"

        sorted_exts = sorted(self.selected_exts)
        return ", ".join(sorted_exts).lstrip('.').upper()

    def browse_input_path(self):
        """根据选择的输入类型弹出不同的对话框"""
        if self.input_type_folder.isChecked():
            # 选择文件夹
            path = QFileDialog.getExistingDirectory(self, "选择输入目录")
        else:
            # 选择文件
            path, _ = QFileDialog.getOpenFileName(
                self, "选择输入文件", "",
                "媒体文件 (*.mp3 *.flac *.wav *.ogg *.opus *.m4a *.aac *.wma "
                "*.mp4 *.mkv *.avi *.mov *.flv *.wmv *.webm *.3gp *.m4v *.mpg *.mpeg *.vob *.ts)"
            )
        if path:
            self.input_path_edit.setText(path)

    def browse_output_dir(self):
        path = QFileDialog.getExistingDirectory(self, "选择输出目录")
        if path:
            self.output_dir_edit.setText(path)

    def browse_cover(self):
        path, _ = QFileDialog.getOpenFileName(
            self, "选择封面图片", "", "图片文件 (*.jpg *.jpeg *.png)"
        )
        if path:
            self.cover_path_edit.setText(path)

    def get_config(self):
        return {
            "input_path": self.input_path_edit.text(),
            "output_dir": self.output_dir_edit.text() or None,
            "output_fmt": self.output_fmt_combo.currentText(),
            "bitrate": self.bitrate_combo.currentText(),
            "same_format_action": self.same_format_action_combo.currentText().lower(),
            "sample_rate": self.sample_rate_combo.currentText(),
            "channels": self.channels_combo.currentText(),
            "threads": self.threads_combo.currentText(),
            "cover_path": self.cover_path_edit.text(),
            "selected_exts": self.selected_exts,
            # 计算最终倍速 = 基础 × 超级
            "speed": float(self.speed_combo.currentText()) *
                     float(self.super_speed_combo.currentText()),
            "tags": {
                "title": self.title_edit.text() or None,
                "artist": self.artist_edit.text() or None,
                "album": self.album_edit.text() or None,
                "year": self.year_edit.text() or None,
                "encoded_by": self.encoded_by_edit.text() or None
            }
        }

    def start_conversion(self):
        if not self.input_path_edit.text():
            QMessageBox.warning(self, "输入错误", "请输入输入路径")
            return

        self.log_text.clear()
        self.progress_bar.setValue(0)
        self.log_message("开始处理...")
        self.log_message(f"文件类型: {self.get_selected_exts_text()}")
        self.log_message("提示：文件名冲突时会自动重命名（添加序号）")
        self.open_dir_btn.setEnabled(False)  # 重置打开目录按钮状态

        config = self.get_config()

        # 处理采样率、声道数和线程数的转换
        config["sample_rate"] = 0 if config["sample_rate"] == "保持原样" else int(config["sample_rate"])
        config["channels"] = 0 if config["channels"] == "保持原样" else int(config["channels"][0])
        config["threads"] = 0 if config["threads"] == "自动" else int(config["threads"])

        # 显示配置信息
        self.log_message(f"输出格式: {config['output_fmt']}")
        self.log_message(f"同格式处理方式: {config['same_format_action']}")
        self.log_message(f"比特率: {config['bitrate']}")

        self.start_btn.setEnabled(False)
        self.cancel_btn.setEnabled(True)

        self.conversion_thread = ConversionThread(config)
        self.conversion_thread.progress_updated.connect(self.update_progress)
        self.conversion_thread.conversion_complete.connect(self.conversion_finished)
        self.conversion_thread.error_occurred.connect(self.handle_error)
        self.conversion_thread.start()

    def cancel_conversion(self):
        if self.conversion_thread and self.conversion_thread.isRunning():
            self.conversion_thread.cancel()
            self.log_message("正在取消任务...")
            self.cancel_btn.setEnabled(False)

    def update_progress(self, processed, total, message):
        # 更新进度条范围和值
        self.progress_bar.setMaximum(total)
        self.progress_bar.setValue(processed)

        # 更新日志
        self.log_message(message)

        # 更新状态栏
        self.statusBar().showMessage(f"处理中: {processed}/{total} 个文件")

    def conversion_finished(self, message, output_dir):
        self.log_message(message)
        self.output_dir = output_dir  # 保存输出目录

        # 正常文本显示输出目录
        self.log_message("")
        self.log_message("转换完成！")
        self.log_message(f"输出目录: {output_dir}")
        self.log_message("可以使用右下角的【打开输出目录】按钮查看结果")

        # 启用打开目录按钮
        self.open_dir_btn.setEnabled(True)

        self.start_btn.setEnabled(True)
        self.cancel_btn.setEnabled(False)
        self.progress_bar.setValue(self.progress_bar.maximum())
        self.statusBar().showMessage("任务完成")

    def handle_error(self, message):
        self.log_message(f"错误: {message}")
        self.start_btn.setEnabled(True)
        self.cancel_btn.setEnabled(False)
        self.statusBar().showMessage("发生错误")

    def log_message(self, message):
        self.log_text.append(message)
        self.log_text.verticalScrollBar().setValue(
            self.log_text.verticalScrollBar().maximum()
        )

    def open_output_directory(self):
        """打开输出目录按钮点击事件"""
        if self.output_dir and os.path.exists(self.output_dir):
            self.open_directory(self.output_dir)
        else:
            QMessageBox.warning(self, "目录不存在", "输出目录不存在或尚未创建")

    def open_directory(self, path):
        """跨平台打开目录"""
        try:
            if platform.system() == "Windows":
                os.startfile(path)
            elif platform.system() == "Darwin":
                subprocess.run(["open", path])
            else:
                subprocess.run(["xdg-open", path])
            self.log_message(f"已打开目录: {path}")
        except Exception as e:
            self.log_message(f"无法自动打开目录: {e}")
            # 尝试使用文件URL方式打开
            try:
                QDesktopServices.openUrl(QUrl.fromLocalFile(path))
            except Exception as e2:
                self.log_message(f"备用打开方式也失败: {e2}")



if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")

    # 设置全局字体
    font = QFont("Microsoft YaHei", 9)
    app.setFont(font)

    window = AudioConverterApp()
    window.show()
    sys.exit(app.exec_())